<!--
  - 摄像头图像数据处理
 -->

<!-- 视图层 --><!-- 页头(上)页脚(下) -->
<template><header-comp />

<!-- 业务介绍：卡片容器 -->
<uni-card>
  <!-- 标题 -->
  <uni-section
    title="业务介绍"
    subtitle="手机重力感应辅助，后置摄像头采集图像，然后以[H, W, C]张量(矩阵)格式形成表格。"
    type="line"
  >
    <!-- 内容 -->
    <view><text space="ensp">
      {{ camIntro }}
    </text></view>
  </uni-section>
</uni-card>

<!-- 重力感应组件 -->
<gravity-init />

<!-- 图像采集：卡片容器 -->
<uni-card>
  <!-- 标题 -->
  <uni-section
    title="图像采集"
    subtitle="采集图像并以[H, W, C]张量(矩阵)格式生成表格。"
    type="line"
  >
    <!-- 摄像头控制-标题 -->
    <view><text space="ensp">
      摄像头控制：
    </text></view>
    <!-- 摄像头控制-内容 -->
    <!-- 条件渲染：若无法访问摄像头 -->
    <view
      v-if="(camRef.camAccessed != true)"
      class="center"
    >
      <text space="ensp">
        权限不足，无法访问摄像头
      </text>
    </view>
    <!-- 条件渲染：若可以访问摄像头 -->
    <view v-else>
      <!-- 摄像头视频流容器 -->
      <view class="center">
        <!-- 摄像头视频流 -->
        <video
          v-if="camRef.camActived"
          id="video-container"
          width="500rpx" height="500rpx"
          style="width: 500rpx; height: 500rpx"
          autoplay muted :controls="false"
        />
      </view>
      <!-- 按钮容器 -->
      <view class="center">
        <!-- 条件渲染：若摄像头没开 -->
        <!-- 打开摄像头按钮 -->
        <button
          v-if="!camRef.camActived"
          @click="activateCam"
          size="mini" type="primary"
        >
          打开摄像头
        </button>
        <!-- 条件渲染：若摄像头开了 -->
        <!-- 采集图像按钮 -->
        <button
          v-else
          @click="camCapture"
          size="mini" type="primary"
        >
          采集图像数据
        </button>
      </view>
      <!-- 采集后的图像-标题 -->
      <view v-if="camRef.camCaptured">
        <view><text space="ensp">
          采集的图像如下：
        </text></view>
        <!-- 采集后的图像-容器 -->
        <view class="center">
          <!-- 采集后的图像，直接操作DOM，无需条件渲染 -->
          <canvas
            id="cam-canvas" canvas-id="cam-canvas"
            style="width: 400rpx; height: 400rpx;"
          />
        </view>
        <!-- 条件渲染：图像是否已采集 -->
        <!-- 打开图表栏容器及按钮按钮 -->
        <view class="center"><button
          @click="showVisor"
          size="mini" type="primary"
        >
          打开图表栏
        </button></view>
      </view>
    </view>
  </uni-section>
</uni-card>

</template>

<!-- 逻辑层 -->
<script setup>
// 导入tfjs和tfjs-vis库
import * as tf from "@tensorflow/tfjs"
import * as tfvis from "@tensorflow/tfjs-vis"
// 导入vue的onMounted、onDeactivated、ref方法
import { onMounted, onDeactivated, ref } from "vue"
// 导入重力感应初始化组件
import gravityInit from "../index/gravity-init-comp.vue"
// 从入口文件导入前端公共对象、公共方法
// 导入机器学习库的公共对象、公共方法
import { findDom, showVisor } from "../../scripts/app_common.js"

/**
 * @对象
 */
// cam项目的绑定变量
const camRef = ref({
  // cam是否可用
  camAccessed: null,
  // cam是否开启
  camActived: null,
  // 图像是否采集
  camCaptured: null,
})
// 摄像头采集内容枚举器，不涉及视图层，不用ref
let camIterator = null
// 摄像头业务的介绍
const camIntro = `  ● 功能简介：
    借助摄像头采集照片（三维张量数据），三维张量数据获取后通过合并RBG数组降维，以二维矩阵展示（原图尺寸缩小为1/4以提升性能）。
  ● 业务预期：
    以三维张量格式用于卷积神经网络的图像识别训练，计划以水滴接触角实验数据图像作为数据集，借此脚本熟悉视频图像处理流程。
  ● 模型&数据：
    卷积神经网络的相关业务代码正在完善中...
`

/**
 * @启动方法 生命周期钩子，组件加载完毕后执行
 */
onMounted(() => {
  // 检查浏览器是否支持访问mediaDevices对象，且该对象是否有getUserMedia()方法
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    // 若有，则赋值给camAccessed
    camRef.value.camAccessed = true
  }
})

/**
 * @结束方法 生命周期钩子，组件从DOM树中移除时执行
 */
onDeactivated(() => {
  // 看看采集枚举器有没有被定义
  if (camIterator && camIterator.stop) {
    // 若有，则停止摄像头
    camIterator.stop()
  }
})

/**
 * @打开摄像头
 */
async function activateCam() { try {
  // 检查摄像头是否可用
  if (camRef.value.camAccessed) {
    // 开启摄像头标记，以确保可找到摄像头DOM
    camRef.value.camActived = true
    // 若可用，找到视频流DOM
    const videoElement = await findDom("video-container", "video")
    // 直接用tf库的方法启动摄像头图像数据处理
    camIterator = await tf.data.webcam(videoElement, {
      // 优先使用环境(后置)摄像头，而不是user(前置)摄像头
      facingMode: "environment",
      // 从原videoDOM中截取的长宽可能和resize的不一样
      // 这可能会导致图像RBG值不再是整数，而是浮点数
      resizeHeight: 400,
      resizeWidth: 400,
      // 从中间截取
      centerCrop: true,
    })
  }
} catch (error) {
  console.error("启动摄像头时报错：", error)
}}

/**
 * @捕获图像数据
 */
async function camCapture() { try {
  // 视图层的resizeXX参数可能导致图像RBG值不再是整数，需调整为整数
  const tfImg = (await camIterator.capture()).toInt()
  tfImg.print(true)
  // 图像已捕获标记
  camRef.value.camCaptured = true
  // 将张量绘制到canvas上
  await camToPixels(tfImg)
  // 将图像数据绘制成表格
  camRenderTable(tfImg)
  // 最后，为防止内存泄漏，对tfImg进行垃圾回收
  tfImg.dispose()
} catch (error) {
  console.error("捕获图像数据时报错：", error)
}}

/**
 * @绘制图像
 */
async function camToPixels(tfImg) { try {
  // 找到canvas的DOM
  const canvasElement = await findDom("cam-canvas", "canvas")
  // 将张量绘制到canvas上
  await tf.browser.toPixels(tfImg, canvasElement)
} catch (error) {
  console.error("绘制图像时报错：", error)
}}

/**
 * @将图像数据渲染为表格
 */
function camRenderTable(tfImg) { try {
  // 将张量转换为数组
  let tfImgArray = tf.tidy(() => {
    // 先压缩张量到1/8
    const tfImgResized = tf.image.resizeNearestNeighbor(
      tfImg,
      [
        parseInt(tfImg.shape[0] / 8),
        parseInt(tfImg.shape[1] / 8)
      ],
    )
    // 取整，然后转成数组
    const tfImgArray = tfImgResized.cast("int32").arraySync()
    // 返回数组
    return tfImgArray
  })
  // 往每行最左侧加一列行序号数组
  for (let i = 0; i < tfImgArray.length; i++) {
    tfImgArray[i].unshift([i + 1])
  }
  // 弄一个标题数组
  let headerArray = [""]
  for (let i = 1; i < tfImgArray[0].length; i++) {
    headerArray.push(i)
  }
  // 结果可视化
  tfvis.render.table(
    {
      // 容器标签
      tab: "图像数据",
      name: "缩小为1/4的图像RGB矩阵Demo",
    }, {
      // 数据内容
      headers: headerArray,
      values: tfImgArray,
    },
  )
} catch (error) {
  console.error("将图像数据渲染为表格时报错：", error)
}}

</script>
